package org.smarthings.things.management.rest.impl;

import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import javax.ws.rs.Path;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;

import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.osgi.service.component.ComponentContext;
import org.smarthings.things.commons.rest.VersionResource;
import org.smarthings.things.commons.utils.QueryParametersUtils;
import org.smarthings.things.management.Thing;
import org.smarthings.things.management.ThingAction;
import org.smarthings.things.management.ThingActionCollection;
import org.smarthings.things.management.ThingCollection;
import org.smarthings.things.management.ThingProperty;
import org.smarthings.things.management.ThingPropertyCollection;
import org.smarthings.things.management.ThingRegistration;
import org.smarthings.things.management.ThingsRegistry;
import org.smarthings.things.management.event.ThingEventConstants;
import org.smarthings.things.management.metadata.ThingActionMetadata;
import org.smarthings.things.management.metadata.ThingPropertyMetadata;
import org.smarthings.things.management.metadata.provider.ThingActionMetadataProvider;
import org.smarthings.things.management.metadata.provider.ThingPropertyMetadataProvider;
import org.smarthings.things.management.rest.resources.ThingCollectionResource;
import org.smarthings.things.management.rest.resources.ThingEventSubscriptionResource;
import org.smarthings.things.management.rest.resources.ThingRegistrationResource;
import org.smarthings.things.management.rest.resources.ThingResource;
import org.smarthings.things.management.rest.resources.actions.ThingActionCollectionResource;
import org.smarthings.things.management.rest.resources.actions.ThingActionExecutionResource;
import org.smarthings.things.management.rest.resources.actions.ThingActionMetadataCollectionResource;
import org.smarthings.things.management.rest.resources.actions.ThingActionMetadataResource;
import org.smarthings.things.management.rest.resources.actions.ThingActionResource;
import org.smarthings.things.management.rest.resources.actions.ThingMetadataLocatorResource;
import org.smarthings.things.management.rest.resources.properties.ThingPropertyCollectionResource;
import org.smarthings.things.management.rest.resources.properties.ThingPropertyMetadataCollectionResource;
import org.smarthings.things.management.rest.resources.properties.ThingPropertyMetadataResource;
import org.smarthings.things.management.rest.resources.properties.ThingPropertyResource;
import org.smarthings.things.management.rest.util.DOMElementMarshaller;
import org.smarthings.things.management.rest.util.DOMElementUnmarshaller;

@Path("things")
@Component(immediate = true)
@Service(RemoteThingsManagement.class)
public class RemoteThingsManagementImpl implements RemoteThingsManagement {

	@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MANDATORY_UNARY)
	private ThingsRegistry thingsRegistry;

	@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MANDATORY_UNARY)
	private ThingPropertyMetadataProvider thingsPropertyMetadataProvider;

	@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.MANDATORY_UNARY)
	private ThingActionMetadataProvider thingsActionMetadataProvider;

	@Context
	private HttpHeaders headers;

	@Context
	private UriInfo uri;

	private ComponentContext context;
	private Map<String, ThingEventHandler> handlers = new HashMap<String, ThingEventHandler>(20);

	public void bindThingsRegistry(ThingsRegistry thingsRegistry) {
		this.thingsRegistry = thingsRegistry;
	}

	public void unbindThingsRegistry(ThingsRegistry thingsRegistry) {
		this.thingsRegistry = null;
	}

	public void bindThingsPropertyMetadataProvider(
			ThingPropertyMetadataProvider thingsPropertyMetadataProvider) {
		this.thingsPropertyMetadataProvider = thingsPropertyMetadataProvider;
	}

	public void unbindThingsPropertyMetadataProvider(
			ThingPropertyMetadataProvider thingsPropertyMetadataProvider) {
		this.thingsPropertyMetadataProvider = null;
	}

	public void bindThingsActionMetadataProvider(
			ThingActionMetadataProvider thingsActionMetadataProvider) {
		this.thingsActionMetadataProvider = thingsActionMetadataProvider;
	}

	public void unbindThingsActionMetadataProvider(
			ThingActionMetadataProvider thingsActionMetadataProvider) {
		this.thingsActionMetadataProvider = null;
	}

	public void activate(ComponentContext context) {
		this.context = context;
	}

	public void deactivate(ComponentContext context) {
		this.context = null;
	}

	public VersionResource getVersion() throws WebApplicationException {
		return ThingsResourceFactory.getVersion(uri);
	}

	public Response registerThing(ThingRegistrationResource registrationr)
			throws WebApplicationException {
		ThingRegistration registration = new ThingRegistration(registrationr.getName(),
				registrationr.getTags(), registrationr.getProperties(), registrationr.getActions());
		Thing registered = thingsRegistry.registerThing(registration);
		ThingResource created = ThingsResourceFactory.getThing(registered, uri);
		return Response.created(created.getHref()).entity(created).build();
	}

	public Response unregisterThing(String thingUUID) throws WebApplicationException {
		validateThingUUID(thingUUID);
		thingsRegistry.unregisterThing(thingUUID);
		return Response.ok().build();
	}

	public ThingCollectionResource getThings(String filter) throws WebApplicationException {
		try {
			int offset = QueryParametersUtils.getOffset(uri);
			int limit = QueryParametersUtils.getLimit(uri);

			ThingCollection collection = filter != null ? thingsRegistry.filterThings(filter)
					: thingsRegistry.listThings();
			return ThingsResourceFactory.getThingCollection(//
					collection.paginator(offset, limit).first(), offset, limit, uri);
		} catch (RuntimeException e) {
			throw new WebApplicationException(e);
		}
	}

	public ThingResource getThing(String thingUUID) throws WebApplicationException {
		Thing thing = validateThingUUID(thingUUID);
		return ThingsResourceFactory.getThing(thing, uri);
	}

	@Override
	public int getThingStateSimple(String thingUUID) throws WebApplicationException {
		Thing thing = validateThingUUID(thingUUID);
		return thing.getState();
	}

	@Override
	public int updateThingSateSimple(String thingUUID, int state) throws WebApplicationException {
		Thing thing = validateThingUUID(thingUUID);
		thing.setState(state);
		return state;
	}

	public ThingPropertyCollectionResource getThingProperties(String thingUUID)
			throws WebApplicationException {
		Thing thing = validateThingUUID(thingUUID);
		ThingPropertyCollection collection = validateThingPropertyCollection(thing);
		return ThingsResourceFactory.getThingPropertyCollection(thing, collection.listProperties(),
				uri);
	}

	public ThingPropertyResource getThingProperty(String thingUUID, String propertyUUID)
			throws WebApplicationException {
		Thing thing = validateThingUUID(thingUUID);
		ThingProperty property = validateThingPropertyUUID(thing, propertyUUID);
		return ThingsResourceFactory.getThingProperty(thing, property, uri);
	}

	@Override
	public String getThingPropertySimple(String thingUUID, String propertyUUID)
			throws WebApplicationException {
		Thing thing = validateThingUUID(thingUUID);
		ThingProperty property = validateThingPropertyUUID(thing, propertyUUID);
		return (property.getValue() == null ? null : property.getValue().toString());
	}

	public ThingPropertyMetadataResource getThingPropertyMetadata(String thingUUID,
			String propertyUUID) throws WebApplicationException {
		Thing thing = validateThingUUID(thingUUID);
		ThingProperty property = validateThingPropertyUUID(thing, propertyUUID);
		return ThingsResourceFactory.getThingPropertyMetadata(thing, property, uri);
	}

	public ThingPropertyResource updateThingProperty(String thingUUID, String propertyUUID,
			ThingPropertyResource updated) throws WebApplicationException {
		Thing thing = validateThingUUID(thingUUID);
		ThingProperty property = validateThingPropertyUUID(thing, propertyUUID);
		try {
			property.setValue(DOMElementUnmarshaller.unmarshalPropertyValue(updated.getValue(),
					property.getMetadata()));
		} catch (Exception e) {
			throw new WebApplicationException(e);
		}
		return ThingsResourceFactory.getThingProperty(thing, property, uri);
	}

	@Override
	public String updateThingPropertySimple(String thingUUID, String propertyUUID, String updated)
			throws WebApplicationException {
		Thing thing = validateThingUUID(thingUUID);
		ThingProperty property = validateThingPropertyUUID(thing, propertyUUID);
		try {
			property.setValue(DOMElementUnmarshaller.unmarshalPropertyValue(updated,
					property.getMetadata()));
		} catch (Exception e) {
			throw new WebApplicationException(e);
		}
		return updated;
	}

	public ThingActionCollectionResource getThingActions(String thingUUID)
			throws WebApplicationException {
		Thing thing = validateThingUUID(thingUUID);
		ThingActionCollection collection = validateThingActionCollection(thing);
		return ThingsResourceFactory.getThingActionCollection(thing, collection.listActions(), uri);
	}

	public ThingActionResource getThingAction(String thingUUID, String actionUUID)
			throws WebApplicationException {
		Thing thing = validateThingUUID(thingUUID);
		ThingAction action = validateThingActionUUID(thing, actionUUID);
		return ThingsResourceFactory.getThingAction(thing, action, uri);
	}

	public ThingActionMetadataResource getThingActionMetadata(String thingUUID, String actionUUID)
			throws WebApplicationException {
		Thing thing = validateThingUUID(thingUUID);
		ThingAction action = validateThingActionUUID(thing, actionUUID);
		return ThingsResourceFactory.getThingActionMetadata(thing, action, uri);
	}

	public Response executeThingAction(String thingUUID, String actionUUID,
			ThingActionExecutionResource arguments) throws WebApplicationException {
		Thing thing = validateThingUUID(thingUUID);
		ThingAction action = validateThingActionUUID(thing, actionUUID);
		try {
			action.execute(DOMElementUnmarshaller.unmarshalThingActionArguments(
					arguments.getArguments(), action.getMetadata()));
		} catch (Exception e) {
			throw new WebApplicationException(e);
		}
		return Response.ok().build();
	}

	@Override
	public Response executeThingActionSimple(String thingUUID, String actionUUID,
			Map<String, String> arguments) throws WebApplicationException {
		Thing thing = validateThingUUID(thingUUID);
		ThingAction action = validateThingActionUUID(thing, actionUUID);
		try {
			action.execute(DOMElementUnmarshaller.unmarshalThingActionArguments(arguments,
					action.getMetadata()));
		} catch (Exception e) {
			throw new WebApplicationException(e);
		}
		return Response.ok().build();
	}

	@Override
	public Map<String, String> getLastThingActionExecutionSimple(String thingUUID, String actionUUID)
			throws WebApplicationException {
		Thing thing = validateThingUUID(thingUUID);
		ThingAction action = validateThingActionUUID(thing, actionUUID);
		return DOMElementMarshaller.marshalThingActionArguments(action.getLastExecutionArguments());
	}

	private Thing validateThingUUID(String thingUUID) throws IllegalArgumentException {
		Thing thing = thingsRegistry.getThing(thingUUID);
		if (thing == null) {
			throw new WebApplicationException("No thing with UUID " + thingUUID, Status.NOT_FOUND);
		}
		return thing;
	}

	private ThingProperty validateThingPropertyUUID(Thing thing, String propertyUUID)
			throws IllegalArgumentException {
		ThingPropertyCollection properties = thing.getProperties();
		if (properties == null) {
			throw new WebApplicationException("No properties at all for thing with UUID "
					+ thing.getUUID(), Status.NOT_FOUND);
		}
		ThingProperty property = properties.getPropertyByUUID(propertyUUID);
		if (property == null) {
			throw new WebApplicationException("No property with UUID " + propertyUUID
					+ " for thing with UUID " + thing.getUUID(), Status.NOT_FOUND);
		}
		return property;
	}

	private ThingPropertyCollection validateThingPropertyCollection(Thing thing)
			throws IllegalArgumentException {
		ThingPropertyCollection properties = thing.getProperties();
		if (properties == null) {
			throw new WebApplicationException("No properties found for thing with UUID "
					+ thing.getUUID(), Status.NOT_FOUND);
		}
		return properties;
	}

	private ThingAction validateThingActionUUID(Thing thing, String actionUUID)
			throws IllegalArgumentException {

		ThingActionCollection actions = thing.getActions();
		if (actions == null) {
			throw new WebApplicationException("No actions at all for thing with UUID "
					+ thing.getUUID(), Status.NOT_FOUND);
		}
		ThingAction action = actions.getActionByUUID(actionUUID);
		if (action == null) {
			throw new WebApplicationException("No action with UUID " + actionUUID
					+ " for thing with UUID " + thing.getUUID(), Status.NOT_FOUND);
		}
		return action;
	}

	private ThingActionCollection validateThingActionCollection(Thing thing)
			throws IllegalArgumentException {

		ThingActionCollection actions = thing.getActions();
		if (actions == null) {
			throw new WebApplicationException("No actions found for thing with UUID "
					+ thing.getUUID(), Status.NOT_FOUND);
		}
		return actions;
	}

	// ////////////////////////////////////////////////////////////

	public Response subscribeThingRegisteredUnregistered(ThingEventSubscriptionResource subscription)
			throws WebApplicationException {
		String filter = subscription.getFilter();
		return subscribeThingEvents(filter, subscription.getCallback(), uri,
				ThingEventConstants.TOPIC_THING_REGISTERED,
				ThingEventConstants.TOPIC_THING_UNREGISTERED);
	}

	public Response unsubscribeThingRegisteredUnregistered(String subscriptionUUID)
			throws WebApplicationException {
		return unsubscribeThingEvents(subscriptionUUID);
	}

	public Response subscribeThingChanged(String thingUUID,
			ThingEventSubscriptionResource subscription) throws WebApplicationException {
		String filter = subscription.getFilter();
		String thingUUIDfilter = '(' + ThingEventConstants.PROPERTY_THING_UUID + '+' + thingUUID
				+ ')';
		filter = filter == null ? thingUUIDfilter : //
				'(' + '&' + thingUUIDfilter + '(' + filter + ')' + ')';

		return subscribeThingEvents(filter, subscription.getCallback(), uri,
				ThingEventConstants.TOPIC_THING_STATE_CHANGED);
	}

	public Response unsubscribeThingChanged(String thingUUID, String subscriptionUUID)
			throws WebApplicationException {
		return unsubscribeThingEvents(subscriptionUUID);
	}

	public Response sunscribeThingPropertyChanged(String thingUUID, String propertyUUID,
			ThingEventSubscriptionResource subscription) throws WebApplicationException {
		String filter = subscription.getFilter();
		String thingUUIDfilter = '(' + ThingEventConstants.PROPERTY_THING_UUID + '+' + thingUUID
				+ ')';
		String propertyUUIDfilter = '(' + ThingEventConstants.PROPERTY_THING_CHANGED_PROPERTY_UUID
				+ '+' + propertyUUID + ')';
		filter = filter == null ? '(' + '&' + thingUUIDfilter + propertyUUIDfilter + ')' : //
				'(' + '&' + thingUUIDfilter + propertyUUIDfilter + filter + ')';

		return subscribeThingEvents(filter, subscription.getCallback(), uri,
				ThingEventConstants.TOPIC_THING_REGISTERED,
				ThingEventConstants.TOPIC_THING_UNREGISTERED);
	}

	public Response unsunscribeThingPropertyChanged(String thingUUID, String propertyUUID,
			String subscriptionUUID) throws WebApplicationException {
		return unsubscribeThingEvents(subscriptionUUID);
	}

	private Response subscribeThingEvents(String filter, URI callback, UriInfo uri,
			String... topics) {
		String subscriptionUUID = UUID.randomUUID().toString();
		ThingEventHandler handler = new ThingEventHandler(callback, filter, topics);
		ThingEventSubscriptionResource created = ThingsResourceFactory.getThingEventSubscription(
				subscriptionUUID, filter, callback, uri);
		handlers.put(subscriptionUUID, handler);
		handler.register(context);
		return Response.created(created.getHref()).entity(created).build();
	}

	private Response unsubscribeThingEvents(String subscriptionUUID) {
		ThingEventHandler handler = handlers.remove(subscriptionUUID);
		if (handler == null) {
			return Response.status(Response.Status.NOT_FOUND).build();
		}
		handler.unregister(context);
		return Response.ok().build();
	}

	@Override
	public ThingMetadataLocatorResource locateThingsMetadata() throws WebApplicationException {
		return ThingsResourceFactory.getThingMetadataLocator();
	}

	@Override
	public ThingPropertyMetadataCollectionResource getThingPropertiesMetadata()
			throws WebApplicationException {
		return ThingsResourceFactory
				.getThingPropertyMetadataCollection(thingsPropertyMetadataProvider);
	}

	@Override
	public Response registerThingPropertyMetadata(String propertyName,
			ThingPropertyMetadataResource created) throws WebApplicationException {
		ThingPropertyMetadata propertyMetadata = DOMElementUnmarshaller
				.unmarshalThingPropertyMetadata(created);
		thingsPropertyMetadataProvider
				.registerThingPropertyMetadata(propertyName, propertyMetadata);
		ThingPropertyMetadataResource registered = ThingsResourceFactory.getThingPropertyMetadata(
				propertyName, thingsPropertyMetadataProvider);
		return Response.created(registered.getHref()).entity(registered).build();
	}

	@Override
	public ThingPropertyMetadataResource getThingPropertyMetadata(String propertyName)
			throws WebApplicationException {
		validateThingPropertyName(propertyName);
		return ThingsResourceFactory.getThingPropertyMetadata(propertyName,
				thingsPropertyMetadataProvider);
	}

	@Override
	public Response unregisterThingPropertyMetadata(String propertyName)
			throws WebApplicationException {
		thingsPropertyMetadataProvider.unregisterThingPropertyMetadata(propertyName);
		return Response.ok().build();
	}

	@Override
	public ThingPropertyMetadataResource updateThingPropertyMetadata(String propertyName,
			ThingPropertyMetadataResource updated) throws WebApplicationException {
		// TODO implement
		return null;
	}

	@Override
	public ThingActionMetadataCollectionResource getThingActionsMetadata()
			throws WebApplicationException {
		return ThingsResourceFactory.getThingActionMetadataCollection(thingsActionMetadataProvider);
	}

	@Override
	public Response registerThingActionMetadata(String actionName,
			ThingActionMetadataResource created) throws WebApplicationException {
		ThingActionMetadata actionMetadata = DOMElementUnmarshaller
				.unmarshalThingActionMetadata(created);
		thingsActionMetadataProvider.registerThingActionMetadata(actionName, actionMetadata);
		ThingActionMetadataResource registered = ThingsResourceFactory.getThingActionMetadata(
				actionName, thingsActionMetadataProvider);
		return Response.created(registered.getHref()).entity(registered).build();
	}

	@Override
	public ThingActionMetadataResource getThingActionMetadata(String actionName)
			throws WebApplicationException {
		validateThingActionName(actionName);
		return ThingsResourceFactory.getThingActionMetadata(actionName,
				thingsActionMetadataProvider);
	}

	@Override
	public Response unregisterThingActionMetadata(String actionName) throws WebApplicationException {
		thingsActionMetadataProvider.unregisterThingActionMetadata(actionName);
		return Response.ok().build();
	}

	@Override
	public ThingActionMetadataResource updateThingActionMetadata(String actionName,
			ThingActionMetadataResource updated) throws WebApplicationException {
		// TODO implement
		return null;
	}

	private void validateThingPropertyName(String propertyName) {
		List<String> properties = thingsPropertyMetadataProvider.getThingProperties();
		if (properties == null || !properties.contains(propertyName)) {
			throw new WebApplicationException("No metadata for property with name " + propertyName,
					Status.NOT_FOUND);
		}
	}

	private void validateThingActionName(String actionName) {
		List<String> actiosns = thingsActionMetadataProvider.getThingActions();
		if (actiosns == null || !actiosns.contains(actionName)) {
			throw new WebApplicationException("No metadata for action with name " + actionName,
					Status.NOT_FOUND);
		}
	}

}
